<?php
/**
 * @file 3gplex/includes/module.inc
 *
 * 
 */
require_once('./'. drupal_get_path('module', 'drupalplex') .'/includes/dbitem.inc');
require_once('./'. drupal_get_path('module', 'drupalplex') .'/includes/exception.inc');

/**
 * 
 */
define("PMOD_LACKING_DEPENDANCE", "Can't configure module %module:
	It depends upon module %module which does not exist");
/**
 * 
 */
define("PMOD_INVALID_DEPENDANCE", "Can't configure module %module:
	It depends upon module %module whose installation failed");
/**
 * Invalid settings.
 * 
 * Either the xml file or the settings given to configuration functions are not
 * compatible with (expressed the way it should for) this module. It could be a missing required
 * property, reference to non existing data, requirement for a module which could not be
 * installed...
 */
define("PMOD_INVALID_SETTINGS", "Cannot configure module %module:
	Invalid settings");
/**
 * Settings were validated however it was not possible to apply them.
 * 
 * Some runtime errors cannot be checked when validating settings or were not
 * present when validating (maybe the settings refer to a date (node type, vocabulary
 * term... whatever) which does not exist anymore.
 */
define("PMOD_INTERNAL_PROBLEM", "Cannot configure module %module:
	Internal problem");
/**
 * XML file is non-existent, unreadable or not an XML file.
 */
define("PMOD_NOSUCH_XML_FILE", "Cannot configure module %module using file %dependance:
	Unable to read xml");
/**
 * XML file is invalid (badly).
 * 
 * While configuring module we do try to convert the xml file to an array of settings to apply.
 * This process failed. It means hard errors in xml file such as empty tree, config file
 * versioning error and so on.
 */
define("PMOD_XML2ARRAY_FAILED", "Cannot configure module %module using file %dependance:
	Invalid config file");
/**
 * Some prerequisites for the Drupal module lack.
 */
define("PMOD_LACK_PREREQUISITES", "Cannot install drupal module %module:
	Some prerequisites are not fulfilled");
/**
 * Failed to install drupal module.
 */
define("PMOD_DRUPAL_INSTALL_FAILED", "Failed to install drupal module %module");
/**
 * Module is already configured.
 */
define("PMOD_CONFIGURED", "Won't configure module %module as it is already configured");
/**
 * Module is not configured.
 */
define("PMOD_UNCONFIGURED", "Won't unconfigure module %module as it is not configured");
/**
 * Drupal module not installed, configuration tries may fail because of this.
 *
 * Configuring a module automatically attempts to install Drupal module before
 * plexer. However testing configuration does not. So tests may not be realised
 * in full conditions. You can install and enable the Drupal module from
 * regular interface prior to running test again.
 */
define("PMOD_NONINSTALLED", "Attempting to test to configure module %module but
Drupal %module isn't installed, if tests fail it *could* be the only reason");

class plex_module_exception extends plex_exception
{
	function __construct($message, $module, $dependance = null)
	{
		$this->message = $message;
		$this->arguments = array('%module'=>$module);
		if (!is_null($dependance))
			$this->arguments['%dependance'] = $dependance;
	}
}

/**
 * Plexable module and module collection handle. 
 */
class plex_module extends plex_dbitem
{
	/** Default directory for plexers. */
	static $default_plexer_dir = null;
	/** Default directory for plexures. */
	static $default_plexure_dir = null;
	/** PHP < 5.3.0 doesn't permit to access a constant using a variable. So override also this function. */
	function get_description() { return ""; }
	/** List of required modules for this one to operate. Override if you have to. */
	protected $requires = array();
	/** Returns list of required modules for this one to operate. */
	function get_requires() { return $this->requires; }
	/** List of modules which require this one to operate. */
	protected $required = array();
	/** Returns list of modules which require this one to operate. */
	function get_required() { return $this->required; }
	function add_required($name) { $this->required[$name] = $name; }
	/** List of modules you should install for this one to operate conveniently. */
	protected $recommended = array();
	/** Returns list of modules you should install for this one to operate conveniently. */
	function get_recommended() { return $this->recommended; }
	/** List of modules you could install to benefit from additional features from this one. */
	protected $suggested = array();
	/** Returns list of modules you could install to benefit from additional features from this one. */
	function get_suggested() { return $this->suggested; }
	/**
	 * Module columns.
	 */
	private static $columns = array(
			"mid"		=> "int unsigned not null auto_increment primary key",
			"module"	=> "varchar(64) not null unique key",
			"plexer_path"	=> "varchar(256) not null",
			"plexure_path"	=> "varchar(256) not null",
			"plexed"	=> "int(1) not null",
			"settings"	=> "blob not null"
	);
	/**
	 * Module columns getter.
	 */
	protected function get_columns() { return self::$columns; }
	/**
	 * Constructor override.
	 */
	function __construct()
	{
		parent::__construct("plex_module", "mid", array("module", "plexer_path", "plexure_path", "plexed", "settings"));
		$this->plexer_path = "";
		$this->plexure_path = "";
		$this->settings = array();
		$this->plexed = 0;
	}
	
	function __toString()
	{
		return $this->module;
	}

	function read_settings($value)
	{
		return unserialize($value);
	}

	function write_settings($value)
	{
		return serialize($value);
	}

	/**
	 * Does set path to a plexer file and updates db item.
	 * 
	 * If $path is null then default paths are looked for.
	 * If $path is a directory a file named {module_name}.inc will be
	 * looked for.
	 * If resulting plexer file is unreadable or noone was found db item won't
	 * be updated.
	 */
	function update_plexer($path = null)
	{
		if ($path === null) {
			$path = self::$default_plexer_dir."/{$this->module}.inc";
		}
		if (is_dir($path)) {
			$array = file_scan_directory($path, "{$this->module}.inc");
			if (count($array) > 0) {
				$this->plexer_path = $array[0]['filename'];
				$this->update();
			}
		} else if (is_file($path)) {
			$this->plexer_path = $path;
			$this->update();
		}
	}

	/**
	 * Does set path to a plexure file and updates db item.
	 * 
	 * If $path is null then default paths are looked for.
	 * If $path is a directory a file named {module_name}.xml will be
	 * looked for.
	 * If resulting plexure file is unreadable or noone was found db item won't
	 * be updated.
	 */
	function update_plexure($path = null)
	{
		if ($path === null) {
			$path = self::$default_plexure_dir."/{$this->module}.xml";
		}
		if (is_dir($path)) {
			$array = file_scan_directory($path, $mod.'.xml');
			if (count($array) > 0) {
				$this->plexure_path = $array[0]['filename'];
				$this->update();
			}
		} else if (is_file($path)) {
			$this->plexure_path = $path;
			$this->update();
		}
	}


	/**
	 * myself overriding.
	 * 
	 * Rather than return "new self" as most other derivations of plex_dbitem do
	 * this override tries to load plex handler for a certain module and return
	 * specialised object (e.g. a plex_contact object for the contact module and
	 * not a plain plex_module).
	 * 
	 * Also it stores instances of specialised module objects so that plex_contact
	 * and friends act (of course) like a singleton (it is up to you to never
	 * load plex_contact yourself).
	 */
	protected function myself(array $row)
	{
		static $instances = array();
		$module_class = "plex_$row[module]";

		if (!isset($instances[$module_class])) {
			try {
				if (is_null($row)) throw new Exception;
				if (!@file_exists($row['plexer_path'])) throw new Exception;
				if (!(@include_once($row['plexer_path']))) throw new Exception;
				if (!class_exists($module_class)) throw new Exception;;
				$new = new ReflectionClass($module_class);
				$new = call_user_func(array($new, 'newInstance'));
				if (!is_object($new)) throw new Exception;
				$instances[$module_class] = $new;
			} catch (Exception $e) {
				return new self;
			}
		}
		return $instances[$module_class];
	}

	/**
	 * 
	 */
	function handled()
	{
		return get_class($this) === "plex_module" ? false : true;
	}

	/**
	 *
	 */
	function get_resume()
	{
		$resume = "{$this->get_description()}";
		$requires = $this->get_requires();
		if (!empty($requires)) {
			$resume .= "<br/>Requires: ".join(', ', (array)$this->get_requires());
		}
		$required = $this->get_required();
		if (!empty($required)) {
			if (empty($requires)) {
				$resume .= "<br/>Required by: ".join(', ', (array)$this->get_required());
			} else {
				$resume .= ". Required by: ".join(', ', (array)$this->get_required());
			}
		}
		return $resume;
	}

	/**
	 * If you want custom validation of the XML file for your module override
	 * this function.
	 * 
	 * @nore beware that plex::simplexml2array() returns a string if xml tree
	 * does not contain any tag (other than root). Cast it's return value to
	 * array.
	 */
	protected function build_settings_xml(SimpleXMLElement $tree)
	{
		return (array)plex::simplexml2array($tree);
	}

	/**
	 * If you want custom validation of the properties array for your module override
	 * this function.
	 */
	protected function validate_settings(array &$settings)
	{
		return true;
	}

	/**
	 * Here is the configuration code for your module, override
	 * this function.
	 */
	protected function apply_settings(array &$settings)
	{
		return true;
	}

	/**
	 * Here is the code to call when unenabling your module, override
	 * this function.
	 */
	protected function revert_settings(array &$settings)
	{
		return true;
	}

	static protected function require_drupal($module)
	{
		if (!module_exists($module)) {
			require_once("./includes/install.inc");
			/*
			 * drupal_install_modules does not check modules'
			 * requirements.
			 */
			if (drupal_check_module($module) === false) {
				throw new plex_module_exception(PMOD_LACK_PREREQUISITES, $module);
			}
			/*
			 * drupal_install_modules does verify a module is
			 * already installed itself.
			 */
			drupal_install_modules((array)$module);
			/*
			 * However it does call module_enable only if installing.
			 */
			module_enable((array)$module);
			if (!module_exists($module)) {
				throw new plex_module_exception(PMOD_DRUPAL_INSTALL_FAILED, $module);
			}
		}
	}

	static protected function validate_drupal($module)
	{
		if (!module_exists($module)) {
			require_once("./includes/install.inc");
			if (drupal_check_module($module) === false) {
				throw new plex_module_exception(PMOD_LACK_PREREQUISITES, $module);
			}
			plex::notice(PMOD_NONINSTALLED, array('%module'=>$module));
		}
	}

	/**
	 * Configure the module given an array of settings.
	 *
	 * $properties will pass validation hook if any then will be given to configuration hook if any.
	 */
	function configure(array $settings)
	{ try {
		if ($this->plexed != 0) {
			throw new plex_module_exception(PMOD_CONFIGURED, $this->module);
		}
		$modules = plex_modules::get_instance();
		foreach ($this->get_requires() as $requires) {
			self::require_drupal($requires);
			if (isset($modules->$requires)) {
				$modules->$requires->add_required($this->module);
				if ($modules->$requires->plexed === 0) {
					if (!$modules->$requires->configure_xml())
						throw new plex_module_exception(PMOD_INVALID_DEPENDANCE, $this->module, $requires);
				}
			}
		}
		self::require_drupal($this->module);
		try { $this->validate_settings($settings); } catch (plex_exception $err) {
			$err->display();
			throw new plex_module_exception(PMOD_INVALID_SETTINGS, $this->module);
		}
		try { $this->apply_settings($settings); } catch (plex_exception $err) {
			$err->display();
			throw new plex_module_exception(PMOD_INTERNAL_PROBLEM, $this->module);
		}
		$this->settings = $settings;
		$this->plexed = 1;
		$this->update();
		plex::notice("Module {$this} is correctly installed and configured");
		return true;
	} catch (plex_exception $err) {
		$err->display();
		return false;
	} }

	/**
	 * Tests configuration of module without side effects.
	 *
	 * @note Without side effects means: without side effects if documentation
	 * was read.
	 * @note The settings member is updated if tests succeeded, you can browse
	 * which configuration settings would be applied from edit/drupalplex/{module}.
	 */
	function validate_xml($file = null)
	{ try {
		if ($this->plexed != 0) {
			throw new plex_module_exception(PMOD_CONFIGURED, $this->module);
		}
		if ($file === null) $file = $this->plexure_path;
		$content = simplexml_load_file($file);
		if ($content === false) {
			throw new plex_module_exception(PMOD_NOSUCH_XML_FILE, $this->module, $file);
		}
		$settings = $this->build_settings_xml($content);
		if ($settings === null) {
			throw new plex_module_exception(PMOD_XML2ARRAY_FAILED, $this->module, $file);
		}
		foreach ($this->get_requires() as $requires) {
			self::validate_drupal($requires);
			if (isset($modules->$requires) && $modules->$requires->plexed === 0) {
				if (!$modules->$requires->validate_xml())
					throw new plex_module_exception(PMOD_INVALID_DEPENDANCE, $this->module, $requires);
			}
		}
		self::validate_drupal($this->module);
		$this->validate_settings($settings);
		$this->settings = $settings;
		$this->update();
		return true;
	} catch (plex_exception $err) {
		$err->display();
		plex::warn(PMOD_INVALID_SETTINGS, array('%module'=>$this->module));
		return false;
	} }

	/**
	 * Configure the module given an xml file.
	 *
	 * $file will be parsed using SimpleXML. The XML to settings array hook will be called
	 * if any otherwise the whole XML file will simply be converted to an array.
	 */
	function configure_xml($file = null)
	{ try {
		if ($this->plexed != 0) {
			throw new plex_module_exception(PMOD_CONFIGURED, $this->module);
		}
		if ($file === null) $file = $this->plexure_path;
		$content = simplexml_load_file($file);
		if ($content === false) {
			throw new plex_module_exception(PMOD_NOSUCH_XML_FILE, $this->module, $file);
		}
		$settings = $this->build_settings_xml($content);
		if ($settings === null) {
			throw new plex_module_exception(PMOD_XML2ARRAY_FAILED, $this->module, $file);
		}
		$this->plexure_path = $file;
		return $this->configure($settings);
	} catch (plex_exception $err) {
		$err->display();
		return false;
	} }
	
	/**
	 * Unconfigure the module.
	 */
	function unconfigure()
	{
		if ($this->plexed != 1) {
			plex::warn(PMOD_UNCONFIGURED, array('%name'=>$this->module));
			return;
		}
		$this->revert_settings($this->settings);
		$this->plexed = 0;
		$this->settings = array();
		$this->update();
	}
}


/**
 * @todo Move these to plex_modules and make the latter a singleton.
 */
plex_module::$default_plexer_dir = "./".drupal_get_path('module', 'drupalplex')."/plexers";
plex_module::$default_plexure_dir = "./".drupal_get_path('module', 'drupalplex')."/plexures";


/**
 * 
 */
class plex_modules extends plex_module implements Iterator
{
	private $items = array();

	function rewind() { reset($this->items); }
	function current() { return current($this->items); }
	function key() { return key($this->items); }
	function next() { return next($this->items); }
	function valid() { return ($this->current() !== false); }

	/**
	 * 
	 */
	function __isset($module)
	{
		return isset($this->items["$module"]);
	}

	/**
	 *
	 */
	function __get($module)
	{
		if (($m = $this->lookup($module))) return $m;
		return $this;
	}

	/**
	 * Clear (cache of) list of modules.
	 */
	function clear()
	{
		$this->items = array();
	}


	static private $instance = null;

	static function get_instance()
	{
		if (self::$instance === null) {
			self::$instance = new self;
		}
		return self::$instance;
	}

	function __construct()
	{
		if (self::$instance !== null) {
			throw new plex_debug_exception();
		}
		parent::__construct();
		$this->create();
		//$this->update_schema();
		foreach ((array)$this->select() as $item) {
			$this->items["$item"] = $item;
		}
	}


	/**
	 * 
	 */
	function __call($name, $args)
	{
		return false;
	}
	
	/**
	 *
	 */
	function handled()
	{
		return false;
	}


	/**
	 * Scan filesystem for available modules.
	 */
	function update($plexer_dir = null, $plexure_dir = null)
	{
		if ($plexer_dir === null) $plexer_dir = self::$default_plexer_dir;
		if ($plexure_dir === null) $plexure_dir = self::$default_plexure_dir;

		$this->clear();

		$plexers = file_scan_directory($plexer_dir, '\.inc$', array('.', '..', 'CVS'), 0, true, 'filename');
		foreach ($plexers as $path => $file) {
			$name = basename($path, '.inc');
			$this->lookup($name, $plexer_dir, $plexure_dir);
		}
		foreach ((array)$this->select() as $item) {
			if (!$item->handled() && $item->plexed == 0) {
				$item->delete();
			}
		}
	}

	/**
	 * Given a name and possible overrides of default search directories does
	 * load .
	 *
	 */
	function lookup($name, $plexer_path = null, $plexure_path = null)
	{
		do {
		/*
		 * If list already has an item for it, return it.
		 * Handled or not, updated or not: This method is here
		 * to add new items, not to update them.
		 */
		if (isset($this->items["$name"])) break;
		/*
		 * Modules namespace is PHP variable name one. However
		 * only '/' could break safety.
		 */
		if (strstr("/", $name)) return null;
		/*
		 * Try to load handled module using db informations.
		 * If this fail we'll create an empty module, insert it to
		 * db then retry.
		 */
		$match = $this->select($name, "module='%s'");
		if (!empty($match)) {
			$this->items["$name"] = $match[0];
			break;
		}
		/*
		 * Look for plexer. If there isn't just quit.
		 */
		if ($plexer_path === null) {
			$plexer_path = self::$default_plexer_dir."/{$name}.inc";
		}
		if (is_dir($plexer_path)) {
			if (list($filename) = each(file_scan_directory($plexer_path, "{$name}\.inc"))) {
				$new = new plex_module;
				$new->module = $name;
				$new->plexer_path = $filename;
			} else {
				return null;
			}
		} else if (is_file($plexer_path)) {
			$new = new plexer_module;
			$new->module = $name;
			$new->plexer_path = $plexer_path;
		} else {
			return null;
		}
		/*
		 * Look for plexure. If there isn't forget about it it isn't
		 * mandatory.
		 */
		$new->plexure_path = "";
		if ($plexure_path === null) {
			$plexure_path = self::$default_plexure_dir."/{$name}.xml";
		}
		if (is_dir($plexure_path)) {
			if (list($filename) = each(file_scan_directory($plexure_path, "{$name}\.xml"))) {
				$new->plexure_path = $filename;
			}
		} else if (is_file($plexure_path)) {
			$new->plexure_path = $plexure_path;
		}
		/*
		 * Sets last initial parameters then insert new module in db *and* list.
		 */
		$new->settings = array();
		$new->plexed = 0;
		$new->insert(true);
		$this->items["$name"] = $new;
		/*
		 * Now retry to load handled module. If this fails we still have plain
		 * unhandled module to return.
		 */
		$match = $this->select($name, "module='%s'");
		if (!empty($match)) $this->items["$name"] = $match[0];
		
		} while (0);
		
		return $this->items["$name"];
	}

	function load($name)
	{
		try {
			$module = $this->lookup($name);
			if ($module === null) {
				self::require_drupal($name);
			} elseif ($module->plexed != 1) {
				$module->configure_xml();
			}
			return true;
		} catch (plex_exception $err) {
			$err->display();
			return false;
		}
	}

}

